---
title: Statesman local setup
---

Statesman serves state storage + Terraform Cloud-compatible APIs. The UI uses its internal endpoints, so enable webhook auth and sync your org/user.

## First-time setup (SQLite migrations)

If using SQLite with persistence, run migrations before starting:

```bash
cd taco

# If you get checksum errors, regenerate the hash first:
atlas migrate hash --dir file://migrations/sqlite

# Apply migrations (adjust path to match your OPENTACO_SQLITE_DB_PATH)
atlas migrate apply --dir file://migrations/sqlite --url "sqlite://cmd/statesman/data/taco.db"
```

If you don't have Atlas installed: `make atlas-install`

## Quick start

1. Set env vars:
   ```bash
   OPENTACO_ENABLE_INTERNAL_ENDPOINTS=statesman-secret   # must match UI STATESMAN_BACKEND_WEBHOOK_SECRET
   OPENTACO_AUTH_DISABLE=true                            # skips OIDC for local
   OPENTACO_STORAGE=memory                               # default; uses SQLite query backend automatically
   # Optional: OPENTACO_SECRET_KEY for signed URLs; OPENTACO_PORT=8080
   ```
2. Run the service (from repo root):
   ```bash
   cd taco
   go run cmd/statesman/main.go -storage memory -auth-disable   # or ./statesman with the same flags
   ```
   Default port: `8080`.

## Sync org and user (required for UI)

Statesman resolves orgs by `external_org_id` (your WorkOS org id). If it cannot resolve, `/internal/api/units` will 500.

A helper script is available at `taco/cmd/statesman/sync-org.sh` - edit the values before running:

```bash
chmod +x taco/cmd/statesman/sync-org.sh
./taco/cmd/statesman/sync-org.sh
```

Or run manually:

```bash
SECRET=$OPENTACO_ENABLE_INTERNAL_ENDPOINTS
ORG_ID=org_xxx                        # WorkOS org id
ORG_NAME=digger-org                   # slug to store
ORG_DISPLAY="Digger Org"
USER_ID=user_xxx                      # WorkOS user id
USER_EMAIL=you@example.com

# create/sync org
curl -s -X POST http://localhost:8080/internal/api/orgs/sync \
  -H "Authorization: Bearer $SECRET" \
  -H "X-User-ID: $USER_ID" -H "X-Email: $USER_EMAIL" \
  -H "Content-Type: application/json" \
  -d '{"name":"'"$ORG_NAME"'","display_name":"'"$ORG_DISPLAY"'","external_org_id":"'"$ORG_ID"'"}'

# ensure user exists in that org
curl -s -X POST http://localhost:8080/internal/api/users \
  -H "Authorization: Bearer $SECRET" \
  -H "X-Org-ID: '$ORG_ID'" -H "X-User-ID: $USER_ID" -H "X-Email: $USER_EMAIL" \
  -H "Content-Type: application/json" \
  -d '{"subject":"'"$USER_ID"'","email":"'"$USER_EMAIL"'"}'
```

## Troubleshooting

- **"no such table: organizations"**: Run migrations first (see First-time setup above).
- **Atlas checksum mismatch**: Run `atlas migrate hash --dir file://migrations/sqlite` then retry apply.
- **403**: webhook secret mismatch (`OPENTACO_ENABLE_INTERNAL_ENDPOINTS` vs UI `STATESMAN_BACKEND_WEBHOOK_SECRET`).
- **404/500 resolving org**: org not synced; rerun the sync script or `orgs/sync` call above.
- **SQLite quirks**: defaults to SQLite in-process; no config needed for local. For Postgres/MySQL, set `TACO_QUERY_BACKEND` and related envs (see `docs/ce/state-management/query-backend`).
